<?php
/* 文字图像
*
*
*/

namespace hpnWse {

require_once(\hpnWse\fGetWseDiry() . 'hpnWse/HttpSvc.php');

use \hpnWse\stNumUtil;
use \hpnWse\stStrUtil;
use \hpnWse\stObjUtil;
use \hpnWse\stAryUtil;
use \hpnWse\stDateUtil;
use \hpnWse\stHttpSvc;


/// SQL实用
class stSqlUtil
{
	/// 日志SQL？若为true则将执行的SQL写入日志
	public static $c_LogSql = false;

	/// 标识符 - 表（`数据库`.`表`）
	/// a_Dtbs: String，数据库，默认null
	/// a_Tab: String，表格，必须有效
	public static function cId_Tbl($a_Dtbs, $a_Tab)
	{
		$l_FullTbl = '`' . $a_Tab . '`';
		if (fBool($a_Dtbs))
		{ $l_FullTbl = '`' . $a_Dtbs . '`.' . $l_FullTbl; }
		return $l_FullTbl;
	}

	/// 标识符 - 列（`列`）
	/// a_Col：String，列
	public static function cId_Col($a_Col)
	{
		return '`' . $a_Col . '`';
	}

	/// 移除危险字符，包括“'"/\”
	public static function cRmvDngrCha($a_Str)
	{
		return preg_replace('/[\'"\\/\\\\]/', '', $a_Str);
	}

	/// 转义值，即"a'b"->"a''b"
	/// a_Wrap：Boolean，包装进引号里？若true则"a'b"->"'a''b'"
	public static function cEscVal($a_Val, $a_Wrap = false)
	{
		$a_Val = str_replace("'", "''", $a_Val);
	//	$a_Val = str_replace('"', '""', $a_Val); // 没必要，外层使用单引号
		if ($a_Wrap) { $a_Val = "'" . $a_Val . "'"; }
		return $a_Val;
	}

	/// 列数组<-列字符串
	public static function cColAryFromStr($a_Cols)
	{
		if (is_string($a_Cols))
		{ $a_Cols = explode(',', $a_Cols); }

		for ($i=0; $i<count($a_Cols); ++$i)
		{
			$a_Cols[$i] = trim($a_Cols[$i]);
		}
		return $a_Cols;
	}

	/// 列字符串<-列数组
	public static function cColStrFromAry($a_Cols)
	{
		return is_string($a_Cols) ? $a_Cols : implode(',', $a_Cols);
	}

	/// 以空格拼接，null将被跳过
	public static function cCcatWithSpc($a___)
	{
		$l_Agms = func_get_args(); $l_AgmAmt = func_num_args();
		$l_Rst = '';
		for ($i=0; $i<$l_AgmAmt; ++$i)
		{
			$l_Agm = $l_Agms[$i];
			if ((null === $l_Agm) || ('' === $l_Agm))
			{ continue; }

			if ($l_Rst) { $l_Rst .= ' '; }
			$l_Rst .= $l_Agm;
		}
		return $l_Rst;
	}

	/// 前加/后加
	/// a_Cols: String，已有的列名，以逗号分隔各个列
	/// a_Col: String，要添加的列名
	/// a_Dir: Number，-1=前加，+1（默认）=后加
	public static function cPpdApd($a_Cols, $a_Col, $a_Dir = +1)
	{
		if ('' === $a_Cols) { return $a_Col; }
		return (-1 == $a_Dir) ? ($a_Col . ',' . $a_Cols) : ($a_Cols . ',' . $a_Col);
	}

	/// 检查是否为int型，不是将抛出异常
	public static function cChkInt($a_Val, $a_ExcMsg = '必须是int')
	{
		if (!is_int($a_Val))
		{ throw new \Exception($a_ExcMsg); }
	}

	/// 检查是否为float型，不是将抛出异常
	public static function cChkFlt($a_Val, $a_ExcMsg = '必须是float')
	{
		if (!is_float($a_Val))
		{ throw new \Exception($a_ExcMsg); }
	}

	/// 过滤列值，主要用于过滤“a_Cvp”参数，本类后面的一些方法接收该参数
	///【注意：PHP里“unset(o[p])”和“o[p]=null”不一样，前者执行后p不会出现在foreach(o)里，后者会】
	/// a_Cvp: Object，列值对儿对象
	/// a_Cn: String，列名
	/// a_fFltr: Any f($a_Cv)，过滤函数，返回过滤后的值，若为null则表示使用默认值
	/// a_Dft: Any，默认值，若为null则表示删除该对儿，以便不会出现在生成的SQL语句里
	/// 返回：若为null则表示a_Cvp中没有属性a_Cn，否则一定有，且属性值就是返回值；注意不要写入a_Cvp！
	public static function cFltrCv(&$a_Cvp, $a_Cn, $a_fFltr, $a_Dft = null)
	{
		// 存在？
		if (isset($a_Cvp[$a_Cn]))
		{
			// 过滤，非null时立即返回，null时使用默认值
			$l_Cv = $a_fFltr($a_Cvp[$a_Cn]);
			if (null !== $l_Cv)
			{ return ($a_Cvp[$a_Cn] = $l_Cv); }

			// 若默认值非null，使用它
			if (null !== $a_Dft)
			{ return ($a_Cvp[$a_Cn] = $a_Dft); }
		}

		// 删除
		unset($a_Cvp[$a_Cn]);
		return null;
	}

	/// 可能是上传的文件名
	///【说明：这里假定对上传文件的命名算法是“md5().tiff”，最少32、最多37个字符】
	public static function cMayBeUpldFlnm($a_Flnm)
	{
		$l_Len = strlen($a_Flnm); // 肯定全是ASCII
		if (($l_Len < 32) || (37 < $l_Len))
		{ return false; }

		return fBool(preg_match('/^[A-Za-z0-9]{32}(?:\.[A-Za-z0-9]+)?$/', $a_Flnm));
	}

	/// 上传URL转成文件名（以便存入数据库）
	/// a_Cvp: Object，列值对儿对象
	/// a_UrlCn: String，URL列名，对应的值是URL，将被转成文件名（以便存入数据库）
	/// a_UrlDiry: String，URL目录，必须从根开始，若URL中不含该子串则使用默认值
	/// a_Dft：String，默认值，null表示当URL无效时从a_Cvp中删除该属性（防止插入或更新时误设置）
	/// 返回：转换后的值（不要写入a_Cvp）
	public static function cUpldUrlToFlnm(&$a_Cvp, $a_UrlCn, $a_UrlDiry, $a_Dft = null)
	{
		return self::cFltrCv($a_Cvp, $a_UrlCn,
			function ($a_Url) use ($a_UrlDiry)
			{
				// 发现危险字符，无效
				if (stStrUtil::cFind($a_Url, '..') >= 0)
				{ return null; }

				// 提取文件名，若不是上传文件名则无效
				$l_Flnm = stStrUtil::cGetFlnm($a_Url);
				if (! self::cMayBeUpldFlnm($l_Flnm))
				{ return null; }

				// 如果有目录
				if ($a_UrlDiry)
				{
					// 确保两端都有正斜杠，若不在指定目录里，无效
					if ('/' !== $a_UrlDiry[0]) { $a_UrlDiry = '/' . $a_UrlDiry; }
					$a_UrlDiry = stStrUtil::cEnsrDiry($a_UrlDiry);

					// 若URL不是以算出的路径为后缀，无效
					$l_UrlPath = $a_UrlDiry . $l_Flnm;
					if (!stStrUtil::cIsSfx($a_Url, $l_UrlPath))
					{ return null; }

					// 按说还应继续检查协议和域名，不过这已足够安全（至多给定的文件不存在），所以就到这里吧！
				}

				// 执行至此，可以返回文件名了
				return $l_Flnm;
			}, $a_Dft);
	}

	/// 获取全部，不存在时返回空数组
	/// a_UseIdx：Boolean，默认false返回关联数组，若true则返回数字索引形式的数组
	public static function cFchAll($a_PdoStmt, $a_UseIdx = false)
	{
		$l_Rst = $a_PdoStmt->fetchAll($a_UseIdx ? \PDO::FETCH_NUM : \PDO::FETCH_ASSOC);
		return $l_Rst;
	}

	/// 获取全部Id，不存在时返回空数组
	///【警告：SQL格式必须为“SELECT Id FROM ...”，即只选出Id，且是无符号整数型】
	/// 返回：uint[]
	public static function cFchAllId($a_PdoStmt)
	{
		$l_Rst = array();
		while (($l_Row = $a_PdoStmt->fetch(\PDO::FETCH_NUM)))
		{ $l_Rst[] = intval($l_Row[0]); }
		return $l_Rst;
	}

	/// 获取一个，不存在时返回null
	public static function cFchOne($a_PdoStmt)
	{
		return \hpnWse\fV1OrV2($a_PdoStmt->fetch(\PDO::FETCH_ASSOC), null);
	}

	/// 获取数字，不存在时返回null
	public static function cFchNum($a_PdoStmt)
	{
		$l_Row = $a_PdoStmt->fetch(\PDO::FETCH_NUM); // “["0"]”之类
		return $l_Row ? intval($l_Row[0]) : null; // 空数组->false
	}

	/// 查询
	/// 返回：query()的返回值
	public static function cQry($a_Pdo, $a_Sql, $a_Agms = null)
	{
		return self::ePdoStmtExec($a_Pdo, $a_Sql, $a_Agms, true);
	}

	/// 执行
	/// a_PdoStmt: a_UseQry为true时PDO，否则PDOStatement
	/// a_UseQry: Boolean，使用query()？默认使用execute()
	/// 返回：execute()/query()的返回值
	public static function cExec($a_PdoStmt, $a_Sql, $a_Agms = null, $a_UseQry = false)
	{
		return self::ePdoStmtExec($a_PdoStmt, $a_Sql, $a_Agms, $a_UseQry);
	}

	/// 预备、执行，返回影响行数（a_AgmsDim=2时为数组）
	/// a_AgmsDim: Number，参数维度，若为2则循环执行（但只编译一次）
	public static function cPrprExec($a_Pdo, $a_Sql, $a_Agms = null, $a_AgmsDim = 1)
	{
		if (2 == $a_AgmsDim)
		{
			$l_PdoStmt = $a_Pdo->prepare($a_Sql);
			$l_Afcd = array();
			foreach ($a_Agms as $l_Idx => $l_Agm)
			{
				self::ePdoStmtExec($l_PdoStmt, $a_Sql, $l_Agm);
				$l_Afcd[] = $l_PdoStmt->rowCount();
			}
		}
		else
		{
			$l_PdoStmt = self::ePdoToStmt($a_Pdo, $a_Sql, $a_Agms);
			$l_Afcd = $l_PdoStmt->rowCount();
		}
		return $l_Afcd;
	}

	/// 预备、执行、获取多个
	public static function cPrprExecFchAll($a_Pdo, $a_Sql, $a_Agms = null, $a_UseIdx = false)
	{
		return self::cFchAll(self::ePdoToStmt($a_Pdo, $a_Sql, $a_Agms), $a_UseIdx);
	}

	/// 预备、执行、获取多个Id
	public static function cPrprExecFchAllId($a_Pdo, $a_Sql, $a_Agms = null)
	{
		return self::cFchAllId(self::ePdoToStmt($a_Pdo, $a_Sql, $a_Agms));
	}

	/// 预备、执行、获取一个
	public static function cPrprExecFchOne($a_Pdo, $a_Sql, $a_Agms = null)
	{
		return self::cFchOne(self::ePdoToStmt($a_Pdo, $a_Sql, $a_Agms));
	}

	/// 预备、执行、获取数字
	public static function cPrprExecFchNum($a_Pdo, $a_Sql, $a_Agms = null)
	{
		return self::cFchNum(self::ePdoToStmt($a_Pdo, $a_Sql, $a_Agms));
	}

	/// 调用存储过程、返回影响行数
	/// a_Agms：Array，参数，每个元素可以是Number或String，对于后者本函数会cEs
	public static function cCall($a_Pdo, $a_Name, $a_Agms = null)
	{
		$l_PdoStmt = self::eCall($a_Pdo, $a_Name, $a_Agms);
		return $l_PdoStmt->rowCount();
	}

	/// 调用存储过程、获取多个
	public static function cCallFchAll($a_Pdo, $a_Name, $a_Agms = null)
	{
		$l_PdoStmt = self::eCall($a_Pdo, $a_Name, $a_Agms);
		return self::cFchAll($l_PdoStmt);
	}

	/// 调用存储过程、获取多个Id
	public static function cCallFchAllId($a_Pdo, $a_Name, $a_Agms = null)
	{
		$l_PdoStmt = self::eCall($a_Pdo, $a_Name, $a_Agms);
		return self::cFchAllId($l_PdoStmt);
	}

	/// 调用存储过程、获取一个
	public static function cCallFchOne($a_Pdo, $a_Name, $a_Agms = null)
	{
		$l_PdoStmt = self::eCall($a_Pdo, $a_Name, $a_Agms);
		return self::cFchOne($l_PdoStmt);
	}

	/// 调用存储过程、获取数字
	public static function cCallFchNum($a_Pdo, $a_Name, $a_Agms = null)
	{
		$l_PdoStmt = self::eCall($a_Pdo, $a_Name, $a_Agms);
		return self::cFchNum($l_PdoStmt);
	}

	private static function ePdoToStmt($a_Pdo, $a_Sql, $a_Agms)
	{
		if ($a_Agms)
		{
			$l_PdoStmt = $a_Pdo->prepare($a_Sql);
			self::ePdoStmtExec($l_PdoStmt, $a_Sql, $a_Agms);
		}
		else
		{
			$l_PdoStmt = self::ePdoStmtExec($a_Pdo, $a_Sql, $a_Agms, true);
		}
		return $l_PdoStmt;
	}

	public static function eCall($a_Pdo, $a_Name, $a_Agms)
	{
		$l_Sql = 'CALL ';
		$l_Sql .= $a_Name;
		$l_Sql .= '(';
		$l_AgmsLen = $a_Agms ? count($a_Agms) : 0;
		for ($i=0; $i<$l_AgmsLen; ++$i)
		{
			$l_Agm = $a_Agms[$i];
			if ($i > 0)
			{ $l_Sql .= ','; }

			if (null === $l_Agm)
			{ $l_Sql .= 'NULL'; }
			else
			if (is_string($l_Agm))
			{ $l_Sql .= self::cEscVal($l_Agm, true); }
			else
			if (is_numeric($l_Agm))
			{ $l_Sql .= $l_Agm; }
			else
			{ throw new \Exception("无效存储过程参数：" . print_r($l_Agm, true), -1); }
		}
		$l_Sql .= ')';
		return self::ePdoStmtExec($a_Pdo, $l_Sql, null, true);
	}

	private static function ePdoStmtExec($a_PdoStmt, $a_Sql, $a_Agms, $a_UseQry = false)
	{
		if (self::$c_LogSql)
		{
			stHttpSvc::cLog('【SQL】' . $a_Sql);
		}

		try
		{
			++stHttpSvc::$c_SqlQryCnt;
			return $a_UseQry ? $a_PdoStmt->query($a_Sql) : $a_PdoStmt->execute($a_Agms);
		}
		catch (\Exception $a_Exc)
		{
			stHttpSvc::cLog('【异常SQL】' . $a_Sql);
			throw $a_Exc;
		}
	}

	/// 事务
	/// a_fDo: String f($a_Pdo)，执行事务，返回 null/空串 提交，返回错误消息则回滚
	/// a_fHdlExc: Object f($a_Exc)，处理异常（此时已回滚），返回错误响应
	/// 返回：成功时为null，失败时为错误响应
	public static function cTsact($a_Pdo, $a_fDo, $a_fHdlExc)
	{
		try
		{
			$a_Pdo->beginTransaction();
			$l_Err = $a_fDo($a_Pdo);
			if ((null === $l_Err) || ('' === $l_Err))
			{
				$a_Pdo->commit();
			}
			else
			{
				// 因为错误是回调函数返回的，而非WSE生成的，故用-2表示应用程序错误
				throw new \Exception($l_Err, -2);
			}
		}
		catch (\Exception $a_Exc)
		{
			$a_Pdo->rollback();
			return $a_fHdlExc($a_Exc);
		}
		return null;
	}


	//======================================================================== Read（Select）

	

	//【太复杂，不如直接写SQL】
// 	/// 读取列
// 	/// $a_Cols：String，要读的列，用逗号分隔
// 	/// $a_Where：String，条件，关键字可省
// 	/// $a_OrderBy：String，排序，关键字可省
// 	/// $a_Limit：String，范围，关键字可省
// 	/// $a_Agms：Array，实参，默认null
// 	/// 返回：PDOStatement
// 	public static function cReadCols($a_Pdo, $a_Tab, $a_Cols, 
// 									$a_Where = '', $a_OrderBy = '', $a_Limit = '',
// 									$a_Agms = null)
// 	{
// 		if (\hpnWse\fBool($a_Where) && 
// 			('WHERE ' !== strtoupper(\hpnWse\stStrUtil::cSub($a_Where, 0, 6))))
// 		{
// 			$a_Where = 'WHERE ' . $a_Where;
// 		}
// 		if (\hpnWse\fBool($a_OrderBy) &&
// 			('ORDER BY ' !== strtoupper(\hpnWse\stStrUtil::cSub($a_Where, 0, 9))))
// 		{
// 			$a_OrderBy = 'ORDER BY ' . $a_OrderBy;
// 		}
// 		if (\hpnWse\fBool($a_Limit) &&
// 			('LIMIT ' !== strtoupper(\hpnWse\stStrUtil::cSub($a_Where, 0, 6))))
// 		{
// 			$a_Limit = 'LIMIT ' . $a_Limit;
// 		}

// $l_Sql = <<<SQL
// SELECT $a_Cols
// FROM $a_Tab
// $a_Where
// $a_OrderBy
// $a_Limit
// SQL;
// 		
//	...
// 	}

	/// 读取一行
	/// $a_Cols：String，要读的列，用逗号分隔
	/// 返回：见cPrprExecFchOne
	public static function cReadRow($a_Pdo, $a_Tab, $a_Cols, $a_Where = '', $a_OdrBy = '', $a_Lmt = '')
	{
		self::cExtrTypedCols($l_NtCols, $l_IntCols, $l_FltCols, $a_Cols);

// 注意尾空格
$l_Sql = <<<SQL
SELECT $l_NtCols
FROM $a_Tab
$a_Where
$a_OdrBy
$a_Lmt
SQL;

		return self::ePrprExecFchTyped(1, $l_IntCols, $l_FltCols, $a_Pdo, $l_Sql);
	}

	/// 行存在？
	/// $a_Where: String，SQL WHERE 子句
	/// 返回：Boolean
	public static function cIsRowExi($a_Pdo, $a_Tab, $a_Where = '')
	{
		
$l_Sql = <<<SQL
SELECT 1
FROM $a_Tab
$a_Where
SQL;

		return (1 === self::cPrprExecFchNum($a_Pdo, $l_Sql));
	}

	/// 读取多行
	/// $a_Cols：String，要读的列，用逗号分隔
	/// 返回：见cPrprExecFchAll
	public static function cReadRows($a_Pdo, $a_Tab, $a_Cols, $a_Where = '', $a_OdrBy = '', $a_Lmt = '')
	{
		self::cExtrTypedCols($l_NtCols, $l_IntCols, $l_FltCols, $a_Cols);

// 注意尾空格
$l_Sql = <<<SQL
SELECT $l_NtCols
FROM $a_Tab
$a_Where
$a_OdrBy
$a_Lmt
SQL;

		return self::ePrprExecFchTyped(2, $l_IntCols, $l_FltCols, $a_Pdo, $l_Sql);
	}

	/// 读取多行 - 排序
	/// $a_Cols：String，要读的列，用逗号分隔
	/// a_ByCols: String: 根据哪些列、哪种方向排序，见cBldStmt_OrderBy
	/// 返回：见cPrprExecFchAll
	public static function cReadRows_Sort($a_Pdo, $a_Tab, $a_Cols, 
										$a_ByCols)
	{
		self::cExtrTypedCols($l_NtCols, $l_IntCols, $l_FltCols, $a_Cols);
		$l_OdrBy = self::cBldStmt_OrderBy(null, $a_ByCols);

// 注意尾空格
$l_Sql = <<<SQL
SELECT $l_NtCols
FROM $a_Tab
$l_OdrBy
SQL;

		return self::ePrprExecFchTyped(2, $l_IntCols, $l_FltCols, $a_Pdo, $l_Sql, null);
	}

	/// 读取一行 - 比较
	/// $a_Cols：String，要读的列，用逗号分隔
	/// $a_InlnWhere: Boolean，内联WHERE？即不使用预编译+占位符，默认false
	/// 返回：见cPrprExecFchOne
	public static function cReadRow_Cmpr($a_Pdo, $a_Tab, $a_Cols, 
										$a_Col, $a_Opt, $a_Val,
										$a_InlnWhere = false)
	{
		return self::eReadRows_Cmpr(1, $a_Pdo, $a_Tab, $a_Cols, $a_Col, $a_Opt, $a_Val, $a_InlnWhere);
	}

	/// 读取多行 - 比较
	/// $a_Cols：String，要读的列，用逗号分隔
	/// $a_InlnWhere: Boolean，内联WHERE？即不使用预编译+占位符，默认false
	/// 返回：见cPrprExecFchAll
	public static function cReadRows_Cmpr($a_Pdo, $a_Tab, $a_Cols, 
										$a_Col, $a_Opt, $a_Val,
										$a_InlnWhere = false)
	{
		return self::eReadRows_Cmpr(2, $a_Pdo, $a_Tab, $a_Cols, $a_Col, $a_Opt, $a_Val, $a_InlnWhere);
	}

	private static function eReadRows_Cmpr($a_Which, $a_Pdo, $a_Tab, $a_Cols, 
										$a_Col, $a_Opt, $a_Val,
										$a_InlnWhere)
	{
		self::cExtrTypedCols($l_NtCols, $l_IntCols, $l_FltCols, $a_Cols);

// 注意尾空格
$l_Sql = <<<SQL
SELECT $l_NtCols
FROM $a_Tab
WHERE 
SQL;

		$l_Agms = array();
		self::eApdSql_Cmpr($l_Sql, $l_Agms, $a_Col, $a_Opt, $a_Val, $a_InlnWhere);
		return self::ePrprExecFchTyped($a_Which, $l_IntCols, $l_FltCols, $a_Pdo, $l_Sql, $l_Agms);
	}

	/// 读取一行 - 比较2
	/// $a_Cols：String，要读的列，用逗号分隔
	/// $a_InlnWhere: Boolean，内联WHERE？即不使用预编译+占位符，默认false
	/// 返回：见cPrprExecFchOne
	public static function cReadRow_Cmpr2($a_Pdo, $a_Tab, $a_Cols, 
										$a_Col, $a_Opt, $a_Val,
										$a_Col2, $a_Opt2, $a_Val2,
										$a_InlnWhere = false)
	{
		return self::eReadRows_Cmpr2(1, $a_Pdo, $a_Tab, $a_Cols, $a_Col, $a_Opt, $a_Val, $a_Col2, $a_Opt2, $a_Val2, $a_InlnWhere);
	}

	/// 读取多行 - 比较2
	/// $a_Cols：String，要读的列，用逗号分隔
	/// $a_InlnWhere: Boolean，内联WHERE？即不使用预编译+占位符，默认false
	/// 返回：见cPrprExecFchAll
	public static function cReadRows_Cmpr2($a_Pdo, $a_Tab, $a_Cols, 
										$a_Col, $a_Opt, $a_Val,
										$a_Col2, $a_Opt2, $a_Val2,
										$a_InlnWhere = false)
	{
		return self::eReadRows_Cmpr2(2, $a_Pdo, $a_Tab, $a_Cols, $a_Col, $a_Opt, $a_Val, $a_Col2, $a_Opt2, $a_Val2, $a_InlnWhere);
	}

	private static function eReadRows_Cmpr2($a_Which, $a_Pdo, $a_Tab, $a_Cols, 
										$a_Col, $a_Opt, $a_Val,
										$a_Col2, $a_Opt2, $a_Val2,
										$a_InlnWhere)
	{
		self::cExtrTypedCols($l_NtCols, $l_IntCols, $l_FltCols, $a_Cols);

// 注意尾空格
$l_Sql = <<<SQL
SELECT $l_NtCols
FROM $a_Tab
WHERE 
SQL;

		$l_Agms = array();
		self::eApdSql_Cmpr2($l_Sql, $l_Agms, $a_Col, $a_Opt, $a_Val, $a_Col2, $a_Opt2, $a_Val2, $a_InlnWhere);
		return self::ePrprExecFchTyped($a_Which, $l_IntCols, $l_FltCols, $a_Pdo, $l_Sql, $l_Agms);
	}

	/// 读取相邻ID标识的行
	/// a_Tab: String，表名，可以内嵌JOIN语句
	/// a_IdCol: String，ID列名，必须有效
	/// a_RefId：Number，参照ID，必须有效
	/// a_Dir: Number，方向，-1=前一个，+1（默认）=后一个
	/// a_Cols: String，要读取的其他列名，不要有a_IdCol
	/// a_WherePart: String，WHERE部分条件，以“AND”关系拼接，会套一层圆括号
	/// 返回：Object，不存在时为null
	public static function cReadRow_AjcId($a_Pdo, $a_Tab, $a_IdCol, $a_RefId, $a_Dir, $a_Cols = '', $a_WherePart = '')
	{
		$a_Cols = self::cPpdApd($a_Cols, $a_IdCol, -1);
		self::cExtrTypedCols($l_NtCols, $l_IntCols, $l_FltCols, $a_Cols);

		$a_RefId = intval($a_RefId);
		$l_CmprOp = (-1 == $a_Dir) ? '<' : '>';
		$l_SortDir = (-1 == $a_Dir) ? 'DESC' : '';
		if ($a_WherePart) { $a_WherePart = ' AND (' . $a_WherePart . ')'; }

// 注意尾空格
$l_Sql = <<<SQL
SELECT $l_NtCols
FROM $a_Tab
WHERE ($a_IdCol $l_CmprOp $a_RefId) $a_WherePart
ORDER BY $a_IdCol $l_SortDir
LIMIT 1
SQL;

		return self::ePrprExecFchTyped(1, $l_IntCols, $l_FltCols, $a_Pdo, $l_Sql);
	}

	/// 读取多行 - 多对多
	/// $a_Tab2, $a_PkCol2, $a_Cols2：String，表2，主键列，要读的列（以逗号分隔）
	/// $a_RelTab, $a_FkCol1, $a_FkCol2: String，关系表，外键1列（指向表1），外键2列（指向表2）
	/// $a_RelCols: String，要读的关系表的列，默认空
	/// $a_Pk1：Number$Number[]，要读取的表1某行之主键值或其数组，若为数组则a_RelCols必须含有无别名的a_FkCol1
	/// 返回：array()，可以调用cMgeMtm()进行融合
	public static function cReadRows_Mtm($a_Pdo, 
										$a_Tab2, $a_PkCol2, $a_Cols2,
										$a_RelTab, $a_FkCol1, $a_FkCol2, $a_RelCols,
										$a_Pk1)
	{
		$l_IsPk1Ary = is_array($a_Pk1);
		if ($l_IsPk1Ary)
		{
			$a_Pk1 = self::cFltrIdAry($a_Pk1);
			if (0 == count($a_Pk1))
			{ return array(); }

			$l_Where = self::cBldStmt_In("tR.$a_FkCol1", $a_Pk1);
		}
		else
		{
			$a_Pk1 = intval($a_Pk1);
			if ($a_Pk1 <= 0)
			{ return array(); }

			$l_Where = "tR.$a_FkCol1 = $a_Pk1";
		}

		self::cExtrTypedCols($l_NtCols, $l_IntCols, $l_FltCols, $a_Cols2);
		$l_NtCols = self::cPfxColsWithTabAs('t2', $l_NtCols);
		if ($a_RelCols)
		{
			if ($l_IsPk1Ary && (stStrUtil::cIdxOf($a_RelCols, $a_FkCol1) < 0)) // 必须取得表1主键列
			{ $a_RelCols .= ','; $a_RelCols .= $a_FkCol1; }

			self::cExtrTypedCols($l_NtCols2, $l_IntCols2, $l_FltCols2, $a_RelCols);
			$l_NtCols2 = self::cPfxColsWithTabAs('tR', $l_NtCols2);
			$l_NtCols .= ','; $l_NtCols .= $l_NtCols2;
			$l_IntCols .= ','; $l_IntCols .= $l_IntCols2;
			$l_FltCols .= ','; $l_FltCols .= $l_FltCols2;
		}

		self::cExtrTypedCols($l_NtCols, $l_IntCols, $l_FltCols, $l_NtCols);

$l_Sql = <<<SQL
SELECT $l_NtCols
FROM $a_Tab2 AS t2
INNER JOIN $a_RelTab AS tR
ON tR.$a_FkCol2 = t2.$a_PkCol2
WHERE $l_Where
SQL;

		$l_Rst = self::ePrprExecFchTyped(2, $l_IntCols, $l_FltCols, $a_Pdo, $l_Sql);
		return $l_Rst;
	}

	/// 融合多对多
	/// a_Ary: Object[]，融合目标数组，每个元素都是对象
	/// a_Pk：String，a_Ary里的主键列，与a_Fk相对应
	/// a_Pn：String，融合目标的属性名
	/// a_MtmRst：cReadRows_Mtm()的返回值
	/// a_Fk：String，a_MtmRst里的外键列
	public static function cMgeMtm(&$a_Ary, $a_Pk, $a_Pn, $a_MtmRst, $a_Fk)
	{
		$l_Map = array();
		foreach ($a_MtmRst as $l_Idx => $l_Rcd)
		{
			$l_Pk = $l_Rcd[$a_Fk];
			if (!isset($l_Map[$l_Pk]))
			{ $l_Map[$l_Pk] = array(); }
			$l_Map[$l_Pk][] = $l_Rcd;
		}

		foreach ($a_Ary as $l_Idx => $l_Rcd)
		{
			$a_Ary[$l_Idx][$a_Pn] = stObjUtil::cFchPpty($l_Map, $l_Rcd[$a_Pk], array());
		}
	}

	/// 读取行数
	/// 返回：Number
	public static function cReadRowAmt($a_Pdo, $a_Tab, $a_Where = '', $a_Lmt = '')
	{
$l_Sql = <<<SQL
SELECT COUNT(*)
FROM $a_Tab
$a_Where
$a_Lmt
SQL;

		return self::cPrprExecFchNum($a_Pdo, $l_Sql);
	}

	/// 读取行数 - 比较
	/// $a_InlnWhere: Boolean，内联WHERE？即不使用预编译+占位符，默认false
	/// 返回：Number
	public static function cReadRowAmt_Cmpr($a_Pdo, $a_Tab, 
											$a_Col, $a_Opt, $a_Val,
											$a_InlnWhere = false)
	{
// 注意尾空格
$l_Sql = <<<SQL
SELECT COUNT(*)
FROM $a_Tab
WHERE 
SQL;

		$l_Agms = array();
		self::eApdSql_Cmpr($l_Sql, $l_Agms, $a_Col, $a_Opt, $a_Val, $a_InlnWhere);
		return self::cPrprExecFchNum($a_Pdo, $l_Sql, $l_Agms);
	}

	/// 读取行数 - 比较2
	/// $a_InlnWhere: Boolean，内联WHERE？即不使用预编译+占位符，默认false
	/// 返回：Number
	public static function cReadRowAmt_Cmpr2($a_Pdo, $a_Tab, 
											$a_Col, $a_Opt, $a_Val,
											$a_Col2, $a_Opt2, $a_Val2, 
											$a_InlnWhere = false)
	{
// 注意尾空格
$l_Sql = <<<SQL
SELECT COUNT(*)
FROM $a_Tab
WHERE 
SQL;

		$l_Agms = array();
		self::eApdSql_Cmpr2($l_Sql, $l_Agms, $a_Col, $a_Opt, $a_Val, $a_Col2, $a_Opt2, $a_Val2, $a_InlnWhere);
		return self::cPrprExecFchNum($a_Pdo, $l_Sql, $l_Agms);
	}

	/// 为列名加上表格别名前缀
	/// a_As: String$Object，表格别名，不要有“.”；
	/// 	若是Object，则键表示别名，值表示逗号分隔的列名（不要带后缀），没有列出的列将被忽略
	/// a_Cols: String，各个列，以逗号分隔，支持后缀“±”，已经有限定名的不作处理，【警告：不支持前缀“int/float”】
	public static function cPfxColsWithTabAs($a_As, $a_Cols)
	{
		if (!\hpnWse\fBool($a_As) || !\hpnWse\fBool($a_Cols))
		{ return ''; }

		$l_IsAry = is_array($a_As);
		$l_AsFromCol = array();
		if ($l_IsAry)
		{
			foreach ($a_As as $l_As => $l_Cols)
			{
				$l_ColsAry = self::cColAryFromStr($l_Cols);
				foreach ($l_ColsAry as $l_Idx => $l_Col)
				{
					// 补上“±”后缀
					$l_AsFromCol[$l_Col] = $l_AsFromCol[$l_Col . '+'] = $l_AsFromCol[$l_Col . '-'] = $l_As;
				}
			}
		}

		$a_Cols = self::cColAryFromStr($a_Cols);
		for ($i=count($a_Cols)-1; $i>=0; --$i)
		{
			$l_Col = $a_Cols[$i];
			if (stStrUtil::cFind($l_Col, '.') >= 0)
			{ continue; }

			if ($l_IsAry)
			{
				if (isset($l_AsFromCol[$l_Col]))
				{ self::ePfxColWithTabAs($a_Cols, $i, $l_Col, $l_AsFromCol[$l_Col]); }
				else
				{ stAryUtil::cErs($a_Cols, $i); }
			}
			else
			{ self::ePfxColWithTabAs($a_Cols, $i, $l_Col, $a_As); }
		}
		return implode(',', $a_Cols);
	}

	private static function ePfxColWithTabAs(&$a_Cols, $a_Idx, $a_Col, $a_As)
	{
		$l_Rst = '';
	//	$l_SpcIdx = stStrUtil::cFind($a_Col, ' '); //【可能“c_Col AS c_As”】
	//	if ($l_SpcIdx < 0)
	//	{
			$l_Rst = $a_As;
			$l_Rst .= '.';
			$l_Rst .= $a_Col;
	//	}
		// else
		// {
		// 	$l_Rst = stStrUtil::cSub($a_Col, 0, $l_SpcIdx + 1);
		// 	$l_Rst .= $a_As;
		// 	$l_Rst .= '.';
		// 	$l_Rst .= trim(stStrUtil::cSub($a_Col, $l_SpcIdx + 1));
		// }
		$a_Cols[$a_Idx] = $l_Rst;
	}

	/// 把部分列转成int型，因为PDO返回的总是String
	/// a_Cols：String，要转成int的列，用逗号分隔
	/// a_Rows：Object$Object[]，行或行数组
	public static function cIntCols($a_Cols, &$a_Rows)
	{
		if (!$a_Rows) { return; }

		$a_Cols = self::cColAryFromStr($a_Cols);
		if (\hpnWse\fIsAry($a_Rows, true))
		{
			for ($i=0; $i<count($a_Rows); ++$i)
			{
				self::eIntFltCol(0, $a_Cols, $a_Rows[$i]);
			}
		}
		else
		{
			self::eIntFltCol(0, $a_Cols, $a_Rows);
		}
	}

	/// 把部分列转成float型，因为PDO返回的总是String
	/// a_Cols：String，要转成int的列，用逗号分隔
	/// a_Rows：Object$Object[]，行或行数组
	public static function cFltCols($a_Cols, &$a_Rows)
	{
		if (!$a_Rows) { return; }
		
		$a_Cols = self::cColAryFromStr($a_Cols);
		if (\hpnWse\fIsAry($a_Rows, true))
		{
			for ($i=0; $i<count($a_Rows); ++$i)
			{
				self::eIntFltCol(1, $a_Cols, $a_Rows[$i]);
			}
		}
		else
		{
			self::eIntFltCol(1, $a_Cols, $a_Rows);
		}
	}

	private static function eIntFltCol($a_Which, $a_Cols, &$a_Row)
	{
		for ($j=0; $j<count($a_Cols); ++$j)
		{
			$l_Col = $a_Cols[$j];
			if (isset($a_Row[$l_Col]))
			{
				$a_Row[$l_Col] = (0 == $a_Which) ? intval($a_Row[$l_Col]) : floatval($a_Row[$l_Col]);
			}
		}
	}

	/// 提取类型化列，例："C1, int C2, float C3, int C4" -> "C1, C2, C3, C4" + "C2,C4" + "C3"
	/// $a_NtCols: String，无类型列，可用于SELECT语句
	/// $a_IntCols, $a_FltCols：String，int和float列，以逗号分隔，若没有则为空串
	/// $a_Cols：String，列，带有类型信息	
	public static function cExtrTypedCols(&$a_NtCols, &$a_IntCols, &$a_FltCols, $a_Cols)
	{
		if (!$a_Cols)
		{
			$a_NtCols = $a_IntCols = $a_FltCols = '';
			return;
		}

		$a_NtCols = self::cColAryFromStr($a_Cols);
		$l_Len = count($a_NtCols);
		$a_IntCols = array();
		$a_FltCols = array();
		
		$i_Rgx = '/(int\\s+|float\\s+)?\\w+(?:\\s+as\\s+(\\w+))?/i';

		for ($j=0; $j<$l_Len; ++$j)
		{
			$l_NtCol = $a_NtCols[$j];
			$l_IsMch = preg_match($i_Rgx, $l_NtCol, $l_Mch); // 肯定匹配
			$l_As = isset($l_Mch[2]) ? $l_Mch[2] : ''; // AS别名
			if (isset($l_Mch[1]) && $l_Mch[1]) // 可能是null或''
			{
				if ('int' == stStrUtil::cSub($l_Mch[1], 0, 3))
				{
					$a_NtCols[$j] = trim(stStrUtil::cSub($l_NtCol, 3));
					$a_IntCols[] = $l_As ? $l_As : $a_NtCols[$j];
				}
				else
				if ('float' == stStrUtil::cSub($l_Mch[1], 0, 5))
				{
					$a_NtCols[$j] = trim(stStrUtil::cSub($l_NtCol, 5));
					$a_FltCols[] = $l_As ? $l_As : $a_NtCols[$j];
				}
			}
		}
		$a_NtCols = implode(',', $a_NtCols);
		$a_IntCols = implode(',', $a_IntCols);
		$a_FltCols = implode(',', $a_FltCols);
	}

	private static function ePrprExecFchTyped($a_Which, $a_IntCols, $a_FltCols, $a_Pdo, $a_Sql, $a_Agms = null)
	{
		if (1 === $a_Which)
		{ $l_Rst = self::cPrprExecFchOne($a_Pdo, $a_Sql, $a_Agms); }
		else
		{ $l_Rst = self::cPrprExecFchAll($a_Pdo, $a_Sql, $a_Agms); }

		if ($l_Rst)
		{
			if ($a_IntCols) { self::cIntCols($a_IntCols, $l_Rst); }
			if ($a_FltCols) { self::cFltCols($a_FltCols, $l_Rst); }
		}
		return $l_Rst;
	}

	// /// 融合多对多
	// ///【说明】主要用于将查询多对多关系得到的数组融合进对象数组中
	// /// a_ObjAry：Object[]，对象数组，即融合的目的地
	// /// a_ObjPk：String，对象主键，将用于和a_PartPk对比，相等就融合
	// /// a_Key：String，融合键，即融合进每个对象的哪个属性里？一定是数组属性
	// /// a_PartAry：Object[]，部分数组，即融合的来源
	// /// a_PartPk：String，部分主键，将用于和a_ObjPk对比，相等就融合，null表示同a_ObjPk
	// /// a_Which：String，把哪个属性装入a_Key标识的数组里？null表示除a_PartPk外的全部属性
	// public static function cMgeMtm(&$a_ObjAry, $a_ObjPk, $a_Key, &$a_PartAry, $a_PartPk = null, $a_Which = null)
	// {
	// 	foreach ($a_PartAry as $l_OneKey => $l_One)
	// 	{
	// 		$l_Idx = \hpnWse\stAryUtil::cFind($a_ObjAry, 
	// 			function ($a_Ary, $a_Idx, $a_Obj) use($a_ObjPk, $a_PartPk, $l_One)
	// 			{ return $a_Obj[$a_ObjPk] == $l_One[$a_PartPk]; });
	// 		if ($l_Idx >= 0) // 应该能找到！
	// 		{
	// 			if (null === $a_Which)
	// 			{
	// 				$l_Temp = $l_One; // 做个副本
	// 				unset($l_Temp[$a_PartPk]);
	// 				$a_ObjAry[$l_Idx][$a_Key][] = $l_Temp;
	// 			}
	// 			else
	// 			{
	// 				$a_ObjAry[$l_Idx][$a_Key][] = $l_One[$a_Which];
	// 			}
	// 		}
	// 	}
	// }


	//======================================================================== Create（Insert）

	/// 创建一行
	/// $a_Cvp：Object，列值对儿
	/// a_OnDup: 当主键或唯一键重复时：
	///		null（默认）：抛出异常，
	///		"IGNORE"：忽略，
	///		"REPLACE"：替换
	///		CVP（列值对儿）：更新指定的列【暂不支持】
	/// 返回：插入ID，0表示错误或不需要插入
	public static function cCrtRow($a_Pdo, $a_Tab, $a_Cvp, $a_OnDup = null)
	{
		$l_AgmCols = array();
		$l_AgmVals = array();
		foreach ($a_Cvp as $l_C => $l_V)
		{
			$l_AgmCols[] = $l_C;
			$l_AgmVals[] = $l_V;
		}
		$l_AgmColsStr = implode(',', $l_AgmCols);
		$l_PrmColsStr = implode(',', stAryUtil::cNew(count($l_AgmCols), '?'));

		$l_SqlHead; $l_SqlTail;
		self::eBldIstStmtHeadTail($l_SqlHead, $l_SqlTail, $a_OnDup);

$l_Sql = <<<SQL
$l_SqlHead INTO $a_Tab
($l_AgmColsStr)
VALUES
($l_PrmColsStr)
$l_SqlTail
SQL;

		$l_Afcd = stSqlUtil::cPrprExec($a_Pdo, $l_Sql, $l_AgmVals);
		if (1 !== $l_Afcd)
		{ return 0; }

		return $a_Pdo->lastInsertId();
	}

	/// 创建多行
	/// a_Cols：String，列，以逗号分隔
	/// a_Data：Array，数据
	/// a_IsCvp: Boolean，a_Data的每个元素是键值对？true则使用“:占位符”，false则使用“?”
	/// 返回：实际插入数量
	public static function cCrtRows($a_Pdo, $a_Tab, $a_Cols, $a_Data, $a_IsCvp, $a_OnDup = null)
	{
		$l_AgmCols = array();
		$l_PrmCols = array();
		$a_Cols = self::cColAryFromStr($a_Cols);
		foreach ($a_Cols as $l_Idx => $l_C)
		{
			$l_AgmCols[] = $l_C;
			$l_PrmCols[] = $a_IsCvp ? (':' . $l_C) : '?';
		}
		$l_AgmColsStr = implode(',', $l_AgmCols);
		$l_PrmColsStr = implode(',', $l_PrmCols);

		$l_SqlHead; $l_SqlTail;
		self::eBldIstStmtHeadTail($l_SqlHead, $l_SqlTail, $a_OnDup);

$l_Sql = <<<SQL
$l_SqlHead INTO $a_Tab
($l_AgmColsStr)
VALUES
($l_PrmColsStr)
$l_SqlTail
SQL;

		$l_Afcds = self::cPrprExec($a_Pdo, $l_Sql, $a_Data, 2);
		return array_sum($l_Afcds);
	}

	/// 创建多行 - 单指令多数据
	/// a_Cols：String，列，以逗号分隔
	/// a_Data：Array[]$String，当数组时，若每行也是数组则用逗号连接各个元素成字符串，非数组则直接转成字符串，
	///		当字符串时，直接拼入SQL；【警告：在任何情况下，对于字符串，都由调用者负责转义和加引号！】
	/// 返回：实际插入数量
	public static function cCrtRows_Simd($a_Pdo, $a_Tab, $a_Cols, $a_Data, $a_OnDup = null)
	{
		if (!$a_Data)
		{ return; }

		$l_Vals = '';
		if (is_string($a_Data))
		{
			$l_Vals = $a_Data;
		}
		else
		{
			$l_Len = count($a_Data);
			$l_IsAry = is_array($a_Data[0]);
			for ($i=0; $i<$l_Len; ++$i)
			{
				if ($l_Vals) { $l_Vals .= ','; }
				$l_Vals .= '(';
				if ($l_IsAry)
				{ $l_Vals .= implode(',', $a_Data[$i]); }
				else
				{ $l_Vals .= strval($a_Data[$i]); }
				$l_Vals .= ')';
			}
		}
		

		$l_SqlHead; $l_SqlTail;
		self::eBldIstStmtHeadTail($l_SqlHead, $l_SqlTail, $a_OnDup);

$l_Sql = <<<SQL
$l_SqlHead INTO $a_Tab
($a_Cols)
VALUES
$l_Vals
$l_SqlTail
SQL;
		$l_Afcd = self::cPrprExec($a_Pdo, $l_Sql);
		return $l_Afcd;
	}

	/// 创建多行 - 多对多关系
	///【说明】生成SQL：INSERT INTO t (c_Pid, c_Sid) VALUES (a_Pid, a_Sid0), (a_Pid, a_Sid1), ...
	/// a_Cols：String，列数组，以逗号分隔，主ID对应的列必须首先列出
	/// a_PrmrId：Number，主ID
	/// a_ScdrIds：Number[]$Object[]，副ID数组或对象数组
	/// 返回影响行数
	public static function cCrtRows_Mtm($a_Pdo, $a_Tab, $a_Cols, $a_PrmrId, $a_ScdrIds)
	{
		$a_PrmrId = intval($a_PrmrId);
		if (!$a_PrmrId || !$a_ScdrIds)
		{ return 0; }

		$l_ColsAry = self::cColAryFromStr($a_Cols);
		$l_ScdrLen = count($l_ColsAry) - 1;

		$l_Vals = '';
		foreach ($a_ScdrIds as $l_Idx => $l_ScdrId)
		{
			if ($l_Vals) { $l_Vals .= ','; }
			$l_Vals .= '(';
			$l_Vals .= $a_PrmrId;
			$l_Vals .= ',';

			if ($l_ScdrLen > 1)
			{
				$l_SVals = '';
				for ($s=0; $s<$l_ScdrLen; ++$s)
				{
					$l_SV = $l_ScdrId[$l_ColsAry[1 + $s]];
					if ($l_SVals) { $l_SVals .= ','; }
					if ((null === $l_SV))// || ('null' === strtolower($l_SV)))
					{ $l_SVals .= 'NULL'; }
					else
					if (is_numeric($l_SV))
					{ $l_SVals .= $l_SV; }
					else
					{ $l_SVals .= self::cEscVal($l_SV, true); }
				}
				$l_Vals .= $l_SVals;
			}
			else
			{
				$l_Vals .= intval($l_ScdrId);
			}

			$l_Vals .= ')';
		}
		if (!$l_Vals)
		{ return 0; }

$l_Sql = <<<SQL
INSERT INTO $a_Tab
($a_Cols)
VALUES
$l_Vals
SQL;

		return self::cPrprExec($a_Pdo, $l_Sql);
	}

	private static function eBldIstStmtHeadTail(&$a_SqlHead, &$a_SqlTail, $a_OnDup)
	{
		$a_SqlHead = 'INSERT';
		$a_SqlTail = '';
		if (null === $a_OnDup)
		{ }
		else
		if (is_array($a_OnDup))
		{ }
		else
		if ('IGNORE' === strtoupper($a_OnDup))
		{ $a_SqlHead = 'INSERT IGNORE'; }
		else
		if ('REPLACE' === strtoupper($a_OnDup))
		{ $a_SqlHead = 'REPLACE'; }
	}

	//======================================================================== Update

	/// 更新行 - WHERE
	/// $a_Cvp：Object，列值对儿，若值是数组就把元素[0]编码进SQL（调用者必须保证安全）
	/// 返回：Number，影响行数
	public static function cUpdRows_Where($a_Pdo, $a_Tab, $a_Cvp, $a_Where)
	{
		$l_Set;
		$l_Agms;
		self::eBldCvp_Upd($l_Set, $l_Agms, $a_Cvp);

$l_Sql = <<<SQL
UPDATE $a_Tab
SET $l_Set
$a_Where
SQL;

		return self::cPrprExec($a_Pdo, $l_Sql, $l_Agms);
	}

	/// 更新行 - 比较
	///【WHERE ($a_Col $a_Opt $a_Val)】
	/// $a_InlnWhere: Boolean，内联WHERE？即不使用预编译+占位符，默认false
	/// 返回：Number，影响行数
	public static function cUpdRows_Cmpr($a_Pdo, $a_Tab, $a_Cvp, 
										$a_Col, $a_Opt, $a_Val, 
										$a_InlnWhere = false)
	{
		$l_Set;
		$l_Agms;
		self::eBldCvp_Upd($l_Set, $l_Agms, $a_Cvp);

// 注意尾空格
$l_Sql = <<<SQL
UPDATE $a_Tab
SET $l_Set
WHERE 
SQL;

		self::eApdSql_Cmpr($l_Sql, $l_Agms, $a_Col, $a_Opt, $a_Val, $a_InlnWhere);
		return self::cPrprExec($a_Pdo, $l_Sql, $l_Agms);
	}

	/// 更新行 - 比较2
	///【WHERE ($a_Col $a_Opt $a_Val) AND ($a_Col2 $a_Opt2 $a_Val2)】
	/// 返回：Number，影响行数
	public static function cUpdRows_Cmpr2($a_Pdo, $a_Tab, $a_Cvp, 
										$a_Col, $a_Opt, $a_Val, $a_Col2, $a_Opt2, $a_Val2,
										$a_InlnWhere = false)
	{
		$l_Set;
		$l_Agms;
		self::eBldCvp_Upd($l_Set, $l_Agms, $a_Cvp);

// 注意尾空格
$l_Sql = <<<SQL
UPDATE $a_Tab
SET $l_Set
WHERE 
SQL;

		self::eApdSql_Cmpr2($l_Sql, $l_Agms, $a_Col, $a_Opt, $a_Val, $a_Col2, $a_Opt2, $a_Val2, $a_InlnWhere);
		return self::cPrprExec($a_Pdo, $l_Sql, $l_Agms);
	}

	/// 更新行 - ID在候选值里
	///【WHERE ($a_IdColIn IN ($a_ValAry[0], ...))】
	/// $a_IdColIn：String，ID列
	/// $a_ValAry：int[]，候选值数组，将用cFltrIdAry过滤
	/// 返回：Number，影响行数
	public static function cUpdRows_IdIn($a_Pdo, $a_Tab, $a_Cvp, 
										$a_IdColIn, $a_ValAry)
	{
		$a_ValAry = self::cFltrIdAry($a_ValAry);
		if ((0 == count($a_ValAry)))
		{ return 0; }

		$l_Set;
		$l_Agms;
		self::eBldCvp_Upd($l_Set, $l_Agms, $a_Cvp);

// 注意尾空格
$l_Sql = <<<SQL
UPDATE $a_Tab
SET $l_Set
WHERE 
SQL;

		$l_InStmt = self::cBldStmt_In($a_IdColIn, $a_ValAry);
		$l_Sql .= $l_InStmt;
		return self::cPrprExec($a_Pdo, $l_Sql, $l_Agms);
	}

	/// 更新行 - 一个ID相等且另一个ID在候选值里
	///【WHERE ($a_IdColEq = $a_Val) AND ($a_IdColIn IN ($a_ValAry[0], ...))】
	/// $a_IdColEq: String，一个ID列
	/// $a_Val: int，相等值
	/// $a_IdColIn：String，另一个ID列
	/// $a_ValAry：int[]，候选值数组，将用cFltrIdAry过滤
	/// 返回：Number，影响行数
	public static function cUpdRows_IdEqAndIdIn($a_Pdo, $a_Tab, $a_Cvp, 
												$a_IdColEq, $a_Val, $a_IdColIn, $a_ValAry)
	{
		$a_Val = intval($a_Val);
		$a_ValAry = self::cFltrIdAry($a_ValAry);
		if ((0 == $a_Val) || (0 == count($a_ValAry)))
		{ return 0; }

		$l_Set;
		$l_Agms;
		self::eBldCvp_Upd($l_Set, $l_Agms, $a_Cvp);

// 注意尾空格
$l_Sql = <<<SQL
UPDATE $a_Tab
SET $l_Set
WHERE 
SQL;

		self::eApdSql_IdEqAndIdIn($l_Sql, $a_IdColEq, $a_Val, $a_IdColIn, $a_ValAry);
		return self::cPrprExec($a_Pdo, $l_Sql, $l_Agms);
	}

	private static function eBldCvp_Upd(&$a_Set, &$a_Agms, $a_Cvp)
	{
		$a_Set = '';
		$a_Agms = array();
		foreach ($a_Cvp as $l_C => $l_V)
		{
			if ($a_Set) { $a_Set .= ', '; }
			$a_Set .= $l_C;
			if (is_array($l_V))
			{
				$a_Set .= ' = ';
				$a_Set .= $l_V[0];
			}
			else
			{
				$a_Set .= ' = ?';
				$a_Agms[] = $l_V;
			}
		}
	}

	/// 过滤列值对儿
	/// $a_Cols: String$String[]，允许的列
	/// $a_Cvp: Object，列值对儿，只有列值出现在a_Cols中的列值对儿才会保留
	/// 返回：Object，过滤后的列值对儿
	public static function cFltrCvp($a_Cols, $a_Cvp)
	{
		$l_Rst = array();
		$a_Cols = self::cColAryFromStr($a_Cols);
		foreach ($a_Cvp as $l_Col => $l_Val) 
		{
			if (\hpnWse\stAryUtil::cIdxOf($a_Cols, $l_Col) >= 0)
			{ $l_Rst[$l_Col] = $l_Val; }
		}
		return $l_Rst;
	}

	/// 过滤ID数组，将所有元素转成int，并去掉≤0的
	public static function cFltrIdAry($a_IdAry)
	{
		$l_Rst = array();
		$l_Len = count($a_IdAry);
		for ($i=0; $i<$l_Len; ++$i)
		{
			$l_Id = intval($a_IdAry[$i]);
			if ($l_Id > 0)
			{ $l_Rst[] = $l_Id; }
		}
		return $l_Rst;
	}

	/// 动作锁，用于限制同一个用户过于频繁地请求
	///【注意】必须先配置下面的三个静态属性
	/// a_IdCol: String，用户ID列
	/// a_TimeCol: String，时间列
	/// a_Id：用户ID
	/// a_Lmt：Number，限制（秒）
	/// 返回：Boolean，若为true则获得锁（可以继续执行），若为false应当立即结束当前请求
	public static function cActnLock($a_Pdo, $a_Tab, $a_IdCol, $a_TimeCol, $a_Id, $a_Lmt)
	{
$l_Sql = <<<SQL
UPDATE $a_Tab
SET $a_TimeCol = NOW()
WHERE ($a_IdCol = $a_Id) AND (TIMESTAMPDIFF(SECOND, $a_TimeCol, NOW()) >= $a_Lmt)
SQL;

		$l_Afcd = stSqlUtil::cPrprExec($a_Pdo, $l_Sql);
		return (1 === $l_Afcd);
	}


	//======================================================================== Delete

	/// 删除行 - WHERE
	/// 返回：Number，影响行数
	public static function cDltRows_Where($a_Pdo, $a_Tab, $a_Where)
	{
$l_Sql = <<<SQL
DELETE FROM $a_Tab
$a_Where
SQL;

		return self::cPrprExec($a_Pdo, $l_Sql);
	}

	/// 删除行 - 比较
	///【参见cUpdRows_Cmpr】
	/// 返回：Number，影响行数
	public static function cDltRows_Cmpr($a_Pdo, $a_Tab, 
										$a_Col, $a_Opt, $a_Val,
										$a_InlnWhere = false)
	{
// 注意尾空格
$l_Sql = <<<SQL
DELETE FROM $a_Tab
WHERE 
SQL;

		$l_Agms = array();
		self::eApdSql_Cmpr($l_Sql, $l_Agms, $a_Col, $a_Opt, $a_Val, $a_InlnWhere);
		return self::cPrprExec($a_Pdo, $l_Sql, $l_Agms);
	}

	/// 删除行 - 比较2
	///【参见cUpdRows_Cmpr2】
	/// 返回：Number，影响行数
	public static function cDltRows_Cmpr2($a_Pdo, $a_Tab, 
										$a_Col, $a_Opt, $a_Val, $a_Col2, $a_Opt2, $a_Val2,
										$a_InlnWhere = false)
	{
// 注意尾空格
$l_Sql = <<<SQL
DELETE FROM $a_Tab
WHERE 
SQL;

		$l_Agms = array();
		self::eApdSql_Cmpr2($l_Sql, $l_Agms, $a_Col, $a_Opt, $a_Val, $a_Col2, $a_Opt2, $a_Val2, $a_InlnWhere);
		return self::cPrprExec($a_Pdo, $l_Sql, $l_Agms);
	}

	/// 删除行 - ID在候选值里
	///【参见cUpdRows_IdIn】
	/// 返回：Number，影响行数
	public static function cDltRows_IdIn($a_Pdo, $a_Tab, $a_IdColIn, $a_ValAry)
	{
		$a_ValAry = self::cFltrIdAry($a_ValAry);
		if ((0 == count($a_ValAry)))
		{ return 0; }

// 注意尾空格
$l_Sql = <<<SQL
DELETE FROM $a_Tab
WHERE 
SQL;

		$l_InStmt = self::cBldStmt_In($a_IdColIn, $a_ValAry);
		$l_Sql .= $l_InStmt;
		return self::cPrprExec($a_Pdo, $l_Sql);
	}

	/// 删除行 - 一个ID相等且另一个ID在候选值里
	///【参见cUpdRows_IdEqAndIdIn】
	/// 返回：Number，影响行数
	public static function cDltRows_IdEqAndIdIn($a_Pdo, $a_Tab, $a_IdColEq, $a_Val, $a_IdColIn, $a_ValAry)
	{
		$a_Val = intval($a_Val);
		$a_ValAry = self::cFltrIdAry($a_ValAry);
		if ((0 == $a_Val) || (0 == count($a_ValAry)))
		{ return 0; }

// 注意尾空格
$l_Sql = <<<SQL
DELETE FROM $a_Tab
WHERE 
SQL;

		self::eApdSql_IdEqAndIdIn($l_Sql, $a_IdColEq, $a_Val, $a_IdColIn, $a_ValAry);
		return self::cPrprExec($a_Pdo, $l_Sql);
	}

	//======================================================================== 构造语句

	// /// 构造合法列值参数
	// /// a_AgmCols, a_AgmVals: Array，合法的列及其修改值
	// /// a_VldCols: String，列出合法的列，以逗号分隔
	// /// a_Data: Object，前端提交的数据
	// public static function cBldVldCvAgms(&$a_AgmCols, &$a_AgmVals, $a_VldCols, $a_Data)
	// {
	// 	// 跳过不允许的列
	// 	$a_VldCols = self::cColAryFromStr($a_VldCols);
	// 	$a_AgmCols = array();
	// 	$a_AgmVals = array();
	// 	foreach ($a_Data as $l_C => $l_V)
	// 	{
	// 		if (stAryUtil::cIdxOf($a_VldCols, $l_C) < 0)
	// 		{ continue; }

	// 		$a_AgmCols[] = $l_C;
	// 		$a_AgmVals[] = $l_V;
	// 	}
	// }

	private static function eApdSql_Cmpr(&$a_Sql, &$a_Agms, 
										$a_Col, $a_Opt, $a_Val, 
										$a_InlnWhere)
	{
		if ($a_InlnWhere)
		{
			$a_Sql .= self::cCcatWithSpc('(', $a_Col, $a_Opt, $a_Val, ')');
		}
		else
		{
			$a_Sql .= self::cCcatWithSpc('(', $a_Col, $a_Opt, '?', ')');
			$a_Agms[] = $a_Val;
		}
	}

	private static function eApdSql_Cmpr2(&$a_Sql, &$a_Agms, 
										$a_Col, $a_Opt, $a_Val, 
										$a_Col2, $a_Opt2, $a_Val2, 
										$a_InlnWhere)
	{
		if ($a_InlnWhere)
		{
			$a_Sql .= self::cCcatWithSpc('(', $a_Col, $a_Opt, $a_Val, ')', 'AND',
						'(', $a_Col2, $a_Opt2, $a_Val2, ')');
		}
		else
		{
			$a_Sql .= self::cCcatWithSpc('(', $a_Col, $a_Opt, '?', ')', 'AND',
						'(', $a_Col2, $a_Opt2, '?', ')');
			$a_Agms[] = $a_Val;
			$a_Agms[] = $a_Val2;
		}
	}

	//【警告】这里假定$a_ValAry是int[]
	private static function eApdSql_IdEqAndIdIn(&$a_Sql, $a_IdColEq, $a_Val, $a_IdColIn, $a_ValAry)
	{
		$l_InStmt = self::cBldStmt_In($a_IdColIn, $a_ValAry);
		$a_Sql .= self::cCcatWithSpc('(', $a_IdColEq, '=', $a_Val, ')', 'AND', '(', $l_InStmt, ')');
	}

	/// 构建占位符 - 赋值列表，用于INSERT和UPDATE语句
	/// $a_Cols：String，列，以逗号分隔
	/// $a_ForInsert：Boolean，用于INSERT？false表示UPDATE
	public static function cBldPlchds_AsnList($a_Cols, $a_ForInsert)
	{
		$a_Cols = self::cColAryFromStr($a_Cols);
		$l_Plchds = array();
		for ($i=0; $i<count($a_Cols); ++$i)
		{
			$a_Cols[$i] = trim($a_Cols[$i]);
			if ($a_ForInsert)
			{
				$l_Plchds[] = ':' . $a_Cols[$i];
			}
			else
			{
				$l_Plchds[] = $a_Cols[$i] . '=:' . $a_Cols[$i];
			}
		}
		$l_Plchds = implode(',', $l_Plchds);
		return $l_Plchds;
	}

	/// 构建语句 - IN
	/// a_Col：要比较的列
	/// a_Vals：Array，候选值，若为空则返回空串，
	///		【警告：若元素是字符串，由调用者负责转义和加引号！】
	/// a_UseEqWhen1: Boolean，当只有一项时使用“=”？
	public static function cBldStmt_In($a_Col, $a_Vals, $a_UseEqWhen1 = false)
	{
		return self::eBldStmt_In_NotIn(1, $a_Col, $a_Vals, $a_UseEqWhen1);
	}

	/// 构建语句 - NOT IN
	/// a_Col：要比较的列
	/// a_Vals：Array，候选值，若为空则返回空串，
	///		【警告：若元素是字符串，由调用者负责转义和加引号！】
	/// a_UseNeWhen1: Boolean，当只有一项时使用“!=”？
	public static function cBldStmt_NotIn($a_Col, $a_Vals, $a_UseNeWhen1 = false)
	{
		return self::eBldStmt_In_NotIn(0, $a_Col, $a_Vals, $a_UseNeWhen1);
	}

	private static function eBldStmt_In_NotIn($a_Which, $a_Col, $a_Vals, $a_When1)
	{
		$l_Len = count($a_Vals);
		if (0 == $l_Len)
		{ return ''; }

		$l_Rst = '(';
		$l_Rst .= $a_Col;
		if ($a_When1 && (1 == $l_Len))
		{
			$l_Rst .= $a_Which ? ' = ' : ' != ';
			$l_Rst .= $a_Vals[0];
		}
		else
		{
			$l_Rst .= $a_Which ? ' IN (' : ' NOT IN (';
			$l_Rst .= implode(',', $a_Vals);
			$l_Rst .= ')';
		}
		$l_Rst .= ')';
		return $l_Rst;
	}

	/// 构建语句 - ORDER BY
	/// a_VldCols：String$String[]，允许的列，可以是逗号分隔的列字符串，若空表示全部有效
	/// a_ByCols：String，形如"c_Age-,c_Name+"，减号表降序，加号（可省）表升序，无效列将被跳过，若没有有效列则返回空
	/// a_AsPfx：String，别名前缀
	public static function cBldStmt_OrderBy($a_VldCols, $a_ByCols, $a_AsPfx = null)
	{
		$l_Rst = '';
		if (!$a_ByCols)
		{ return $l_Rst; }
		
		if ($a_VldCols)
		{ $a_VldCols = self::cColAryFromStr($a_VldCols); }

		$a_ByCols = self::cColAryFromStr($a_ByCols);
		foreach ($a_ByCols as $l_Idx => $l_ByCol) 
		{
			$l_Len = strlen($l_ByCol);
			if (0 == $l_Len)
			{ continue; }

			$l_Desc = ('-' === $l_ByCol[$l_Len - 1]);
			if ($l_Desc || ('+' === $l_ByCol[$l_Len - 1]))
			{ $l_ByCol = stStrUtil::cSub($l_ByCol, 0, $l_Len - 1); }

			if ($a_VldCols && (stAryUtil::cIdxOf($a_VldCols, $l_ByCol) < 0))
			{ continue; }

			if ($l_Rst) { $l_Rst .= ','; }
			else { $l_Rst = 'ORDER BY '; }

			$l_Rst .= $l_ByCol;
			if ($l_Desc) { $l_Rst .= ' DESC'; }
		}
		return $l_Rst;
	}

	/// 构建语句 - ORDER BY FIELD($a_Col, ...)
	/// a_Col：要排序的列
	/// a_Vals：Array，顺序值，若为空则返回空串，
	///		【警告：若元素是字符串，由调用者负责转义和加引号！】
	public static function cBldStmt_OrderByFld($a_Col, $a_Vals)
	{
		if (!$a_Vals)
		{ return ''; }

		$l_Rst = 'ORDER BY FIELD(';
		$l_Rst .= $a_Col;
		$l_Rst .= ',';
		$l_Rst .= self::cColStrFromAry($a_Vals);
		$l_Rst .= ')';
		return $l_Rst;
	}

}


} // namespace hpnWse

// PDO::lastInsertId

/*
// If you need to set an ISOLATION level or LOCK MODE it needs to be done BEFORE you make the BeginTransaction() call...
 //
 //  **note** you should always check result codes on operations and do error handling.  This sample code
 //  assumes all the calls work so that the order of operations is accurate and easy to see
 //
 //  THIS IS using the PECL PDO::INFORMIX module, running on fedora core 6, php 5.2.4
 //
 //    This is the correct way to address an informix -243 error (could not position within table) when there
 //    is no ISAM error indicating a table corruption.  A -243 can happen (if the table/indexes, etc., are ok) 
 //    if a row is locked.  The code below sets the LOCK MODE to wait 2 minutes (120 seconds) before
 //    giving up.  In this example you get READ COMMITTED rows, if you don't need read committed
 //    but just need to get whatever data is there (ignoring locked rows, etc.) instead of
 //    "SET LOCK MODE TO WAIT 120" you could "SET ISOLATION TO DIRTY READ".
 //
 //    In informix you *must* manage how you do reads because it is very easy to trigger a
 //    lock table overflow (which downs the instance) if you have lots of rows, are using joins
 //    and have many updates happening.  
 //

 // e.g.,

 $sql= "SELECT FIRST 50 * FROM mytable WHERE mystuff=1 ORDER BY myid";                    // define SQL query 

 try                                                                                // create an exception handler
     {
     $dbh = new PDO("informix:host=......");
          
     if ($dbh)    // did we connect?
         {
         $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
         $dbh->query("SET LOCK MODE TO WAIT 120")
         
         # ----------------
         # open transaction cursor
         # ----------------
         if    ( $dbh->beginTransaction() )                                         # explicitly open cursor
             {
             try   //  open exception handler 
                 {
                 $stmt = $dbh->prepare($sql, array(PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL));

                 $stmt->execute();
                 
                 while ($row = $stmt->fetch(PDO::FETCH_NUM, PDO::FETCH_ORI_NEXT))
                     {
                     $data = $row[0] . "\t" . $row[1] . "\t" . $row[2] . "\t" . $row[3] . "\t" . $row[4] . "\t" . $row[5] . "\t" . $row[6] . "\t" . $row[7] . "\n" . $row[8] ;
                     //print $data;
                     print_r($row);
                     };
                 
                 $stmt = null;
                 }
             catch (PDOException $e)
                 {
                 print "Query Failed!\n\n";
                 
                 print "DBA FAIL:" . $e->getMessage();
                 };
             
             $dbh->rollback();                                                       # abort any changes (ie. $dbh->commit()
             $dbh = null;                                                            # close connection
             }
         else
             {
             # we should never get here, it should go to the exception handler
             print "Unable to establish connection...\n\n";
             };
         };
     }
 catch (Exception $e)
     {
     $dbh->rollback();
     echo "Failed: " . $e->getMessage();
     }; 

*/


/* 【PDO调用带有OUT参数的存储过程，待定】

CREATE PROCEDURE `para_out`(out id int)

BEGIN	

   SELECT 5 into id;

   SELECT id;

END





$stmt = $dbh->prepare("CALL para_out(@?)");

$stmt->bindParam(1, $return_value, PDO::PARAM_INT, 8);



// call the stored procedure

if($stmt->execute()==false)

    print_r($stmt->errorInfo());

else

    print "procedure returned $return_value\n";


//返回

Array

(

    [0] => 42000

    [1] => 1414

    [2] => OUT or INOUT argument 1 for routine test.para_out is not a variable or NEW pseudo-variable in BEFORE trigger

)

*/

//////////////////////////////////// OVER ////////////////////////////////////